# -*- coding: utf-8 -*-
#--------------------------------------------------------------------------
# Copyright (c) 2009 Barry Schwartz
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

#--------------------------------------------------------------------------

cimport stdlib
cimport stdio

cdef extern from 'stdio.h' nogil:
    int snprintf(char *str, size_t size, char *format, ...)

cdef extern from 'string.h' nogil:
    void *memcpy(void *dest, void *src, size_t n)
    char *strchr(char *s, int c)

cdef extern from 'math.h' nogil:
    double fabs(double x)

import sys
from pdf_crypto import crypto_params, encrypt_object_data

#--------------------------------------------------------------------------

class pdfobj_params:
    string_wrapper = pdf_hex_string

object_list = []

cdef char *pdf_delimiters = b'()<>[]{}/%'
cdef char *pdf_delimiters_and_poundsign = b'#()<>[]{}/%'

cdef enum:
    backspace     = 0x08
    horiz_tab     = 0x09
    linefeed      = 0x0A
    formfeed      = 0x0C
    carriage_return = 0x0D
    space         = 0x20
    paren_left    = 0x28
    paren_right   = 0x29
    less_than     = 0x3C
    greater_than  = 0x3E
    bracket_left  = 0x5B
    backslash     = 0x5C
    bracket_right = 0x5D

# FIX: Make this work for multiple precision arithmetic.
cpdef double pt_to_bp(double x) except *:
    return x * (7200.0 / 7227.0)

cpdef encode_user_readably(s):
    if not isinstance(s, bytes):
        s = b'\xFE\xFF' + s.encode('UTF-16BE')
    return s

cpdef pdf_null(ignored_):
    return b'null'

cpdef pdf_bool(bint b):
    if b:
        t = b'true'
    else:
        t = b'false'
    return t

cpdef pdf_int(int i):
    cdef char buf[50]
    cdef int count = snprintf(buf, 50, b'%d', i)
    if 50 <= count:
        raise ValueError('Integer too large for pdf_int ' + str(i))
    result = bytes(count)
    <void> memcpy(<char *> result, buf, count)
    return result

cpdef pdf_real(double r, precision = None):
    cdef char buf[50]
    cdef int count
    if precision != None:
        count = snprintf(buf, 50, b'%.*f', <int> precision, <double> r)
    else:
        count = snprintf(buf, 50, b'%.6f', <double> r)
    if 50 <= count:
        raise ValueError('Integer too large for pdf_real ' + str(r))
    result = bytes(count)
    <void> memcpy(<char *> result, buf, count)
    return result

def pdf_numeric(n, precision = None):
    if isinstance(n, int):
        t = pdf_int(n)
    else:
        i = int(n)
        if i == n:
            t = pdf_int(i)
        else:
            t = pdf_real(n, precision)
    return t

cdef make_bytes(int i):
    if i == linefeed:
        return b'\\n'
    elif i == carriage_return:
        return b'\\r'
    elif i == horiz_tab:
        return b'\\t'
    elif i == backspace:
        return b'\\b'
    elif i == formfeed:
        return b'\\f'
    elif i == paren_left:
        return b'\\('
    elif i == paren_right:
        return b'\\)'
    elif i == backslash:
        return b'\\\\'
    else:
        return bytes([i])

non_hex_lookup = [make_bytes(i) for i in range(256)]
non_hex_left   = b'('
non_hex_right  = b')'

hex_lookup     = ['{0:02X}'.format(i).encode('ASCII') for i in range(256)]
hex_left       = b'<'
hex_right      = b'>'

def pdf_string_stringmaker(a):
    return pdf_string(a[0], encryption_allowed = False)

cdef pdf_string_encrypted(s):
    data = [None]
    indirect_obj = indirect(data, pdf_string_stringmaker)
    data[0] = encrypt_object_data(indirect_obj['object_no'], indirect_obj['generation'], s)
    return pdf_reference(indirect_obj)

def pdf_string(s, encryption_allowed = True):
    if encryption_allowed and crypto_params.encryption_algorithm in (2, 4):
        return pdf_string_encrypted(s)
    else:
        t = non_hex_left
        for c in s:
            t += non_hex_lookup[c]
        return t + non_hex_right

def pdf_hex_string(s, encryption_allowed = True):
    if encryption_allowed and crypto_params.encryption_algorithm in (2, 4):
        return pdf_string_encrypted(s)
    else:
        t = hex_left
        for c in s:
            t += hex_lookup[c]
        return t + hex_right

def pdf_name(s):
    cdef int c
    cdef char buf[8]

    if not isinstance(s, bytes):
        s = s.encode('UTF-8')

    t = b'/'
    for c in s:
        if c < 33 or 126 < c or strchr(pdf_delimiters_and_poundsign, c) != NULL:
            snprintf(buf, 8, b'#%02X', c)
        else:
            buf[0] = c
            buf[1] = 0
        t += buf
    return t

def pdf_array(a):
    t = b'['
    for item in a:
        if t[-1] in pdf_delimiters or item[0] in pdf_delimiters:
            t += item
        else:
            t += b' ' + item
    t += b']'
    return t

def pdf_dict(d):
    t = b'<<\n'
    for (key, val) in d.items():
        if val != b'null':
            if val[0] in pdf_delimiters:
                sep = b''
            else:
                sep = b' '
            t = t + pdf_name(key) + sep + val + b'\n'
    t = t + b'>>'
    return t

def pdf_reference(indirect_object):
    return '{0} {1} R'.format(str(indirect_object['object_no']),
                              str(indirect_object['generation'])).encode('ASCII')

def indirect(data, string_maker, generation = 0, object_no = None):
    if object_no != None:
        indirect_object = object_list[object_no - 1]
        indirect_object['data'] = data
        indirect_object['string_maker'] = string_maker
    else:
        object_no = len(object_list) + 1
        indirect_object = {
            'data'         : data,
            'string_maker' : string_maker,
            'object_no'    : object_no,
            'generation'   : generation,
            'offset'       : None,
            }
        object_list.append(indirect_object)
    return indirect_object

def reserve_object_no(generation = 0):
    object_no = len(object_list) + 1
    indirect_object = {
        'data'         : None,
        'string_maker' : None,
        'object_no'    : object_no,
        'generation'   : generation,
        'offset'       : None,
        }
    object_list.append(indirect_object)
    return object_no

#--------------------------------------------------------------------------

def prepare_glyphs(glyphs, fonts, fm):

    widths = fonts[fm['file_name']]['font_info']['advance_widths']
    widths_count = len(widths)

    glyphs_count = len(glyphs)
    prepped_glyphs = [None for i in range(glyphs_count)]

    first_glyph = fm['first_glyph']
    unicodes = fm['unicodes']

    code_to_glyph_index = fm['code_to_glyph_index']

    for i in range(glyphs_count):
        (xg, yg, g, text) = glyphs[i]
        gid = code_to_glyph_index(g)

        (x, y) = (pt_to_bp(xg), pt_to_bp(yg))
        if gid == 0 or widths_count <= gid:
            advance_width = 1000
        else:
            advance_width = widths[gid]

        text_tag = text[0]
        if text_tag == 'ClusterContinuation':
            prepped_glyphs[i] = (x, y, gid, advance_width, 'ClusterContinuation', None)
        elif text_tag == 'GlyphText':
            text_value = text[1]
            tounicode = unicodes[g - first_glyph]#???????????????????????????????????????????????????????????????????????????
            if tounicode != text_value:
                actual_text = pdfobj_params.string_wrapper(encode_user_readably(text[1]),
                                                           encryption_allowed = False)
                prepped_glyphs[i] = (x, y, gid, advance_width, 'ActualText', actual_text)
            else:
                prepped_glyphs[i] = (x, y, gid, advance_width, 'ToUnicode', None)
        else:
            prepped_glyphs[i] = (x, y, gid, advance_width, 'ToUnicode', None)

    return prepped_glyphs

def split_by_font(simple_glyphs):
    glyphs_count = len(simple_glyphs)
    split = []
    i = 0
    while i < glyphs_count:
        glyphs = []
        current_fm = simple_glyphs[i][2][1][1]

        # A more readable version of the slightly more optimized
        # loop below would go like:
        #
        #   while (i < glyphs_count and
        #          simple_glyphs[i][2][1][1] == current_fm):
        #
        while True:
            if glyphs_count <= i:
                break
            sgi = simple_glyphs[i]
            if sgi[2][1][1] != current_fm:
                break
            (x, y, (ignored_, (g, ignored_, text))) = simple_glyphs[i]
            glyphs.append((x, y, g, text))
            i += 1

        split.append((current_fm, glyphs))
    return split

def actual_text_cluster(prepped_glyphs, i, partitioned_glyphs):
    glyphs_count = len(prepped_glyphs)
    j = i + 1
    while j < glyphs_count and prepped_glyphs[j][4] == 'ClusterContinuation':
        j += 1
    return do_partitioning(prepped_glyphs, j, partitioned_glyphs + [prepped_glyphs[i:j]])

def tounicode_cluster(prepped_glyphs, i, partitioned_glyphs):
    glyphs_count = len(prepped_glyphs)
    j = i + 1
    while j < glyphs_count and prepped_glyphs[j][4] == 'ToUnicode':
        j += 1
    return do_partitioning(prepped_glyphs, j, partitioned_glyphs + [prepped_glyphs[i:j]])

def do_partitioning(prepped_glyphs, i, partitioned_glyphs):
    if i == len(prepped_glyphs):
        return partitioned_glyphs
    else:
        (x, y, g, advance_width, mode, actual_text) = prepped_glyphs[i]
        if mode == 'ActualText':
            return actual_text_cluster(prepped_glyphs, i, partitioned_glyphs)
        else:
            return tounicode_cluster(prepped_glyphs, i, partitioned_glyphs)

def partition_glyphs(prepped_glyphs):
    return do_partitioning(prepped_glyphs, 0, [])

def encode_run(run):
    s = b''
    run_length = len(run)
    if run_length == 1:
        s += run[0] + b'Tj'
    elif run_length != 0:
        s += b'['
        for r in run:
            s += r
        s += b']TJ'
    return s

def streamify_partitioned_glyphs(partitioned_glyphs,
                                 double point_size,
                                 glyphs_used):
    cdef double x0, y0, advance_width
    cdef double x1, y1, advance_width1
    cdef double x_adjustment
    cdef unsigned int g

    result = b''
    (x0, y0, advance_width) = (0, 0, 0)
    for partition in partitioned_glyphs:

        if partition[0][4] == 'ActualText':
            result += b'/Span<</ActualText' + partition[0][5] + b'>>BDC '

        run = []
        subrun = bytes()
        for (x1, y1, g, advance_width1, unused1_, unused2_) in partition:
            glyphs_used.add(g)
            x_adjustment = advance_width - 1000.0*(x1 - x0)/point_size
            if y0 != y1:
                if len(subrun) != 0:
                    run.append(pdfobj_params.string_wrapper(subrun, False))
                    subrun = bytes()
                result += encode_run(run)
                run = []
                result += (pdf_numeric((x1 - x0)/point_size) + b' ' +
                           pdf_numeric((y1 - y0)/point_size) + b' Td')
            elif 0.5 < fabs(x_adjustment):
                if len(subrun) != 0:
                    run.append(pdfobj_params.string_wrapper(subrun, False))
                    subrun = bytes()
                run.append(pdf_numeric(x_adjustment, precision = 0))
            subrun += bytes((g // 256,)) + bytes((g % 256,))
            (x0, y0, advance_width) = (x1, y1, advance_width1)
        if subrun != b'':
            run.append(pdfobj_params.string_wrapper(subrun, False))
        result += encode_run(run)

        if partition[0][4] == 'ActualText':
            result += b' EMC'
        result += b'\n'

    return result

#--------------------------------------------------------------------------
